package com.yangyouli.unity_demo_server.server;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.log4j.Logger;

import com.yangyouli.unity_demo_server.container.Container;

public class Server {
	// 每隔多少毫秒检查一次所有客户端的心跳情况
	private static final long CHECK_INTERVAL_TIME = 60000;
	// 超过最大心跳请求间隔时间(单位毫秒)则判定客户端宕机，关闭连接
	private static final long MAX_HEARTBEAT_INTERVAL_TIME = 60000;
	private static final long CLEAN_UP_CODE_INTERVAL_TIME = 1000 * 60 * 10; // 清除验证码的间隔时间
	private static final long VERIFICATION_CODE_EXPIRE_TIME = 1000 * 60 * 10; // 验证码过期时间
	private static final long SEND_EMAIL_INTERVAL_TIME = 1000 * 30; // 发送邮箱的间隔时间
	
	private ServerSocket serverSocket;
	private Set<Client> clients = new TreeSet<>();
	private Map<Integer, Client> loginClientsMap = new TreeMap<>();
	private Map<String, RoomObj> roomMap = new HashMap<>();
	private Map<String, EmailVerificationCode> emailCodeMap = new TreeMap<>(); // 存放邮箱与验证码对象
	private ExecutorService executorService;
	private int port;
	Logger log = Logger.getLogger(Server.class);
	
	/**
	 * 实例化 Server 对象时同时设置端口号
	 * @param port
	 */
	public Server (int port) {
		this.port = port;
		// 启动线程连接池
		executorService = Executors.newCachedThreadPool();
	}
	
	/**
	 * 启动 Server
	 * @throws IOException
	 */
	public void start () {                                  
		try {
			// 初始化依赖注入的容器
			Container.getInstance();
			
			serverSocket = new ServerSocket(port);
			log.info("服务器已经启动");
			
			// 启动心跳检测线程
			new HeartbeatCheckThread().start();
			
			// 启动过期验证码清除线程
			new RemoveExpireCode().start();
			
			// 一直接收请求
			while (true) {
				// 得到客户端的代理 socket
				Socket clientSocket = serverSocket.accept();
				// 实例化 Client 类
				Runnable client = new Client(clientSocket, this);
				synchronized (clients) {
					clients.add((Client) client);
				}
				// 线程代理 client
				executorService.execute(client);
				log.info("客户端" + clientSocket.getInetAddress().getHostAddress() + "已经连接");
			}
		} catch (IOException e) {
			log.error("启动 Server 失败", e);
		}
	}
	
	/**
	 * 发送数据给玩家所在房间的人，如果玩家所在的房间为空，则只发送数据给自己
	 * @param requestClassName
	 * @param data
	 * @param client
	 */
	public void sendToRoom (String requestClassName, String data, Client client) {
		RoomObj roomObj = client.getRoomObj();
		if (roomObj == null) {
			client.sendResponse(requestClassName, data);
			return;
		}
		roomObj.getMaster().sendResponse(requestClassName, data);
		for (Client c : roomObj.getClients()) {
			c.sendResponse(requestClassName, data);
		}
	}
	
	/**
	 * 发送数据给玩家所在房间的人，除了自己，如果房间为空，则不发送
	 * @param requestClassName
	 * @param data
	 * @param roomId
	 * @param excludeClient
	 */
	public void sendToRoomExclude (String requestClassName, String data, Client excludeClient) {
		RoomObj roomObj = excludeClient.getRoomObj();
		if (roomObj == null) return;
		
		if (excludeClient != roomObj.getMaster()) {
			roomObj.getMaster().sendResponse(requestClassName, data);
		}
		for (Client client : roomObj.getClients()) {
			if (client != excludeClient) {
				client.sendResponse(requestClassName, data);
			}
		}
	}
	
	/**
	 * 发送数据给所有客户端
	 * @param requestClassName
	 * @param data
	 */
	public void sendToAllClients (String requestClassName, String data) {
		for (Client client : getLoginClients()) {
			client.sendResponse(requestClassName, data);
		}
	}
	
	/**
	 * 发送数据给所有客户端，除了某个客户端
	 * @param requestClassName
	 * @param data
	 * @param excludeClient
	 */
	public void sendToAllClientsExclude (String requestClassName, String data, Client excludeClient) {
		for (Client client : getLoginClients()) {
			if (client != excludeClient) {
				client.sendResponse(requestClassName, data);
			}
		}
	}
	
	/**
	 * 移除掉客户端的 Client 对象
	 * @param clientSocket
	 */
	public void removeClientSocket (Client client) {
		synchronized (clients) {
			clients.remove(client);
			loginClientsMap.remove(client.getUserId());
		}
	}
	
	/**
	 * 用户登陆，将其添加到已经登陆的用户 map 中，并从未登录 set 中移除在
	 * @param client
	 */
	public void userLogin (Client client) {
		synchronized (clients) {
			clients.remove(client);
			loginClientsMap.put(client.getUserId(), client);
		}
	}
	
	/**
	 * 判断玩家是否在线
	 * @param userId
	 * @return
	 */
	public boolean isUserOnline (int userId) {
		return loginClientsMap.containsKey(userId);
	}
	
	/**
	 * 得到已经登陆的用户的 Client 对象
	 * @param userId
	 * @return
	 */
	public Client getLoginUserClient (int userId) {
		return loginClientsMap.get(userId);
	}
	
	/**
	 * 用户注销，将其从登陆的用户 map 中移除，并添加到未登录 set 中
	 * @param client
	 */
	public void userLogoff (Client client) {
		synchronized (clients) {
			clients.add(client);
			loginClientsMap.remove(client.getUserId());
		}
	}
	
	private int roomIdGenerate = 100;
	/**
	 * 添加房间，房间主人是 master
	 * @param master
	 * @return int 创建的房间号
	 * 		   -1 该玩家已经创建了房间，不能再创建
	 */
	public int createRoom (Client master) {
		// 如果已经创建了房间，则不能再创建
		for(RoomObj r : getRoomObjs()) {
			if (r.getMaster() == master) {
				return -1;
			}
		}
		RoomObj roomObj = new RoomObj();
		roomObj.setMaster(master);
		roomObj.setRoomId(String.valueOf(roomIdGenerate));
		synchronized (roomMap) {
			roomMap.put(String.valueOf(roomIdGenerate), roomObj);
		}
		master.setRoomObj(roomObj);
		log.info("创建房间，房间号为：" + roomIdGenerate);
		return roomIdGenerate++;
	}
	
	/**
	 * 玩家加入房间
	 * @param roomId
	 * @param client
	 * @return 1 添加成功
	 * 		   0 队伍人数等于 最大人数，不能再加入人
	 */
	public int joinRoom (String roomId, Client client) {
		RoomObj roomObj = this.getRoom(roomId);
		if (roomObj == null) return -1;
		// 有一个空位或以上才能加入房间
		if (roomObj.getClients().size() > (roomObj.getMaxPeople() - 2)) return 0;
		synchronized (roomObj) {
			roomObj.addClient(client);
		}
		client.setRoomObj(roomObj);
		return 1;
	}
	
	/**
	 * 得到房间对象
	 * @param roomId
	 * @return
	 */
	public RoomObj getRoom (String roomId) {
		return roomMap.get(roomId);
	}
	
	/**
	 * 删除房间
	 * @param room
	 */
	public void removeRoom (String roomId) {
		synchronized (roomMap) {
			roomMap.remove(roomId);
			log.info("解散房间，房间号为：" + roomId);
		}
	}
	
	/**
	 * 如果玩家在房间中，则移除
	 * @param masterId
	 */
	public void removePlayerFromRoom (Client client) {
		// 是房主，如果房间内有其他玩家，则转让房间，否则解散
		for (RoomObj roomObj : getRoomObjs()) {
			if (roomObj.getMaster().getUserId() == client.getUserId()) {
				synchronized (roomObj) {
					if (roomObj.getClients().size() > 0) {
						// 房间内有其他玩家，转移房间
						roomObj.getPositionIsTaken()[roomObj.getMasterPosition()] = false;
						roomObj.setMaster(roomObj.getClients().get(0), roomObj.getClientsPosition().get(0));
						roomObj.getClients().remove(0);
						roomObj.getClientsPosition().remove(0);
						
					} else {
						// 无其他玩家，解散房间
						removeRoom(roomObj.getRoomId());
					}
				}
			}
		}
		// 是玩家，移除
		if (client.getRoomObj() != null) {
			client.getRoomObj().removeClient(client);
		}
		client.setRoomObj(null);
	}
	
	/**
	 * 检测客户端心跳是否超时的线程
	 * @author 24279
	 */
	private class HeartbeatCheckThread extends Thread {
		@Override
		public void run () {
			log.info("心跳检测线程启动");
			try {
				while (true) {
					Thread.sleep(CHECK_INTERVAL_TIME);
					for(Client client : getAllClients()) {
						long heartbeatInterval = new Date().getTime() - client.getHeartbeatTime();
						if (heartbeatInterval >= MAX_HEARTBEAT_INTERVAL_TIME) {
							log.info(client.getClientSocket().getInetAddress().getHostAddress() + "心跳超时，将被关闭");
							client.closeSocket();
						}
					}
				}
			} catch (InterruptedException e) {
				log.error("心跳检测发生异常", e);
			}
		}
	}
	
	private class RemoveExpireCode extends Thread {
		@Override
		public void run () {
			log.info("过期验证码清除线程启动");
			try {
				while (true) {
					Thread.sleep(CLEAN_UP_CODE_INTERVAL_TIME);
					long currentTime = new Date().getTime();
					for (Map.Entry<String, EmailVerificationCode> entry : emailCodeMap.entrySet()) {
						if (entry.getValue().expireTime < currentTime) {
							// 验证码过期了，移除
							removeVerificationCode(entry.getKey());
						}
					}
				}
			} catch (InterruptedException e) {
				log.error("过期验证码清除线程发生异常", e);
			}
		}
	}
	/**
	 * 验证码对象
	 * @author 24279
	 *
	 */
	private class EmailVerificationCode {
		public String code;
		public long expireTime;
		public long lastSendTime; // 上一次发送验证码的时间
		public EmailVerificationCode (String code, long expireTime, long lastSendTime) {
			this.code = code;
			this.expireTime = expireTime;
			this.lastSendTime = lastSendTime;
		}
	}

	/**
	 * 得到所有已经登陆的客户端对象
	 * @return
	 */
	public Collection<Client> getLoginClients() {
		return loginClientsMap.values();
	}
	
	/**
	 * 得到所有已经连接的客户端对象
	 * @return
	 */
	public Collection<Client> getAllClients () {
		Collection<Client> c = new LinkedList<>();
		c.addAll(getLoginClients());
		c.addAll(clients);
		return c;
	}
	
	public List<RoomObj> getRoomObjs () {
		return new ArrayList<RoomObj>(roomMap.values());
	}
	
	/**
	 * 删除存储的验证码数据
	 * @param email
	 */
	public void removeVerificationCode (String email) {
		synchronized (emailCodeMap) {
			emailCodeMap.remove(email);
		}
	}
	
	/**
	 * 增加验证码信息，如果已经存在，则覆盖
	 * @param email
	 * @param code
	 * @param 客户端发来的 userId，为了验证该邮箱用户是否是该账号的拥有者
	 * @return true 添加成功，false 允许发邮件的间隔时间未到
	 */
	public boolean addVerifcationCode (String email, String code) {
		long currentTime = new Date().getTime();
		EmailVerificationCode e = emailCodeMap.get(email);
		if (e != null && (e.lastSendTime + SEND_EMAIL_INTERVAL_TIME) > currentTime) {
			return false;
		}
		synchronized (emailCodeMap) {
			emailCodeMap.put(email, new EmailVerificationCode(code, 
					currentTime + VERIFICATION_CODE_EXPIRE_TIME, currentTime));
		}
		return true;
	}
	
	/**
	 * 得到邮箱的验证码，如果验证码过期，则返回 null
	 * @param email
	 * @return 数组第一个元素：获取验证码的用户的 userId，第二个元素：验证码，如果不存在或过期了，则返回 null
	 */
	public String getCodeByEmail (String email) {
		EmailVerificationCode e = emailCodeMap.get(email);
		if (e == null) {
			return null;
		}
		if (e.expireTime < new Date().getTime()) {
			// 过期，删除
			removeVerificationCode(email);
			return null;
		}
		return e.code;
	}
}
